#include <stdio.h>
#include <alloca.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include "libzyfp.h"
/*********************| 数据定义 |*********************/
typedef enum{
	AC_SHOWINFO,
	AC_HELP,
	AC_PACK,
	AC_DUMP,
	AC_UNPACK
}Action_t ;

#define	Co_RED		31
#define Co_GREEN	32
#define Co_YELLOW	33
#define Co_BLUE		34
#define Co_WHITE	37

typedef struct {
	const char*	pf;
	const char*	key;
	const char*	dir;
	const char*	src;
}PackParse_t;

// 针对linux的
#ifndef DT_UNKNOWN
#define DT_UNKNOWN	0
#endif
#ifndef DT_DIR
#define DT_DIR		4
#endif
#ifndef DT_REG
#define DT_REG		8
#endif

/*********************| 内部方法 |*********************/
static void cprintf(const char* l, int c)
{
	printf("\033[;%dm%s\033[0m", c, l);
}

static Action_t parse_args(const int argc, const char* argv[], PackParse_t* pp)
{
	pp->dir = pp->key = pp->pf = NULL;
	if (argc < 2 || !strcmp(argv[1], "-h"))
		return AC_HELP;
	if (2 == argc) {
		pp->pf = argv[1];
		return AC_SHOWINFO;
	}
	if (argc < 4)
		return AC_HELP;

	if (0 == strcmp(argv[2], "-key")) {
		if (argc < 6)
			return AC_HELP;
		pp->key = argv[3];
	}

	if (0 == strcmp(argv[1], "-p")) {
		if (pp->key) {
			pp->dir = argv[4];
			pp->pf = argv[5];
		} else {
			pp->dir = argv[2];
			pp->pf = argv[3];
		}
		return AC_PACK;
	} else if (0 == strcmp(argv[1], "-up")) {
		if (pp->key) {
			pp->dir = argv[5];
			pp->pf = argv[4];
		} else {
			pp->dir = argv[3];
			pp->pf = argv[2];
		}
		return AC_UNPACK;
	} else if (0 == strcmp(argv[1], "-d")){
		if (pp->key) {
			if (argc < 7)
				return AC_HELP;
			pp->pf = argv[4];
			pp->src = argv[5];
			pp->dir = argv[6];
		} else {
			if (argc < 5)
				return AC_HELP;
			pp->pf = argv[2];
			pp->src = argv[3];
			pp->dir = argv[4];
		}
		return AC_DUMP;
	}
	return AC_HELP;
}

// 我这个帮助打印得真是优美，呜哈哈
static void show_help()
{
	cprintf("---------------------------| zyfp |---------------------------\n"
		"|zyf package是一种简单的归档文件。可用于简单的资源加密。     |\n"
		"|它收集指定的文件将除目录外的文件内容写入单一文件。并生成一个|\n"
		"|用于用于展开解档的索引文件。                                |\n"
		"|                                张宇飞 zhangyufei49@163.com |\n"
		"--------------------------------------------------------------\n", Co_GREEN);
	cprintf("|<归档文件名>                                                |\n", Co_YELLOW);
	cprintf("|显示归档信息                                                |\n", Co_BLUE);
	cprintf("--------------------------------------------------------------\n", Co_GREEN);
	cprintf("|-h                                                          |\n", Co_YELLOW);
	cprintf("|显示这条帮助信息                                            |\n", Co_BLUE);
	cprintf("--------------------------------------------------------------\n", Co_GREEN);
	cprintf("|-p [-key 密钥] <dir> <outfile>                              |\n", Co_YELLOW);
	cprintf("|将dir下的所有文件归档为文件outfile并生成outfile.zyfpi       |\n", Co_BLUE);
	cprintf("--------------------------------------------------------------\n", Co_GREEN);
	cprintf("|-up <infile> <outdir>                                       |\n", Co_YELLOW);
	cprintf("|根据infile.zyfpi解档infile到outdir下                        |\n", Co_BLUE);
	cprintf("--------------------------------------------------------------\n", Co_GREEN);
	cprintf("|-d [-key 密钥] <归档文件> <要解档的文件名> <outfile>        |\n", Co_YELLOW);
	cprintf("|从归档文件中解档出某个指定文件                              |\n", Co_BLUE);
	cprintf("--------------------------------------------------------------\n", Co_GREEN);
}


static void show_pack_info(const PackParse_t* pp)
{
	zyfp_handle* ph = zyfp_init(pp->pf, NULL, ZYFP_O_READ);
	if (!ph) {
		cprintf("读取归档文件出现错误\n", Co_RED);
		return;
	}
	zyfp_head head;
	int r = zyfp_gethead(ph, &head);
	if (r != ZYFP_ERR_NONE) {
		cprintf("获取包头信息出现错误:", Co_RED);
		printf("%s\n", zyfp_errmsg(r));
		return;
	}
	printf("%s加密的归档文件\n", head.encrypt ? "" : "未");
	printf("程序库版本:   %d\n", ZYFP_LIB_VERSION);
	printf("归档文件版本: %d\n", head.version);
	printf("包含文件数量: %u\n", head.filenum);
	printf("打包时间:     %s\n", ctime(&head.packtime));
	zyfp_end(ph);
}

#define zyfpi_code(pp) \
	char* zyfpi = (char*)alloca(strlen(pp->pf) + 1 + strlen(".zyfpi")); \
	sprintf(zyfpi, "%s.zyfpi", pp->pf)

// 遍历文件夹，向包体添加文件，索引中添加索引
static void add_dir2pack(DIR* dirp, FILE* fi, zyfp_handle* ph, const char* dirname)
{
	struct dirent* pt;
	uint32_t dl = (uint32_t)strlen(dirname);
	if (dl) {
		while (dl --> 0) {
			if ('/' != dirname[dl])
				break;
		}
		++dl;
		if (1 == dl && '.' == dirname[0])
			dl = 0;
	}
	if (dl) {
		fwrite("1", 1, 1, fi);
		fwrite(&dl, sizeof(dl), 1, fi);
		fwrite(dirname, dl, 1, fi);
	}
	char* fullname;
	uint32_t fnl;
	int ret;
	while ((pt = readdir(dirp)) != NULL) {
		if (DT_UNKNOWN == pt->d_type) {
			cprintf("文件系统不支持dirent::d_type的判定", Co_YELLOW);
			return;
		}
		if ((pt->d_type != DT_REG) && (pt->d_type != DT_DIR))
			continue;
		if (!strcmp(pt->d_name, ".") || !strcmp(pt->d_name, ".."))
			continue;
		fnl = dl + 1 + strlen(pt->d_name);
		if (dl) {
			fullname = (char*)alloca(fnl);
			strncpy(fullname, dirname, dl);
			sprintf(fullname + dl, "/%s", pt->d_name);
		} else {
			fullname = pt->d_name;
			fnl -= 1;
		}
		zyfp_log("%s, %u\n", fullname, pt->d_type);
		if (DT_REG == pt->d_type) {
			ret = zyfp_add(ph, fullname, fnl);
			if (ret != ZYFP_ERR_NONE) {
				printf("打包文件%s时出现错误: %s\n", fullname, zyfp_errmsg(ret));
				continue;
			}
			fwrite("0", 1, 1, fi);
			fwrite(&fnl, sizeof(fnl), 1, fi);
			fwrite(fullname, fnl, 1, fi);
			cprintf(".", Co_BLUE);
		} else if (DT_DIR == pt->d_type) {
			DIR* tdir = opendir(fullname);
			if (!tdir) {
				printf("读取文件夹%s失败，请检查权限\n", fullname);
			} else {
				add_dir2pack(tdir, fi, ph, fullname);
				closedir(tdir);
			}
		}
	}
}

static void do_pack(const PackParse_t* pp)
{
	DIR* pd = opendir(pp->dir);
	if (!pd) {
		cprintf("指定路径打开失败, 或许是非文件夹\n", Co_RED);
		return;
	}
	zyfp_handle* ph = zyfp_init(pp->pf, pp->key, ZYFP_O_WRITE);
	if (!ph) {
		cprintf("创建归档文件出现错误\n", Co_RED);
		closedir(pd);
		return;
	}
	// 写入索引文件的文件头4字节的密钥长度，密钥
	uint32_t kl = (uint32_t)strlen(pp->key);
	zyfpi_code(pp);
	FILE* fp = fopen(zyfpi, "wb");
	if (!fp) {
		cprintf("创建解包索引文件出现错误\n", Co_RED);
		closedir(pd);
		zyfp_end(ph);
		unlink(pp->pf);
		return;
	}
	fwrite(&kl, sizeof(uint32_t), 1, fp);
	if (kl)
		fwrite(pp->key, kl, 1, fp);
	cprintf("开始尝试打包，请耐心等待\n", Co_YELLOW);
	add_dir2pack(pd, fp, ph, pp->dir);
	// 关闭文件处理
	fclose(fp);
	closedir(pd);
	if (zyfp_end(ph)) {
		unlink(pp->pf);
		unlink(zyfpi);
		cprintf("未打包任何文件\n", Co_BLUE);
	} else {
		cprintf("\n完成打包工作\n", Co_GREEN);
	}
}

static void do_dump(const PackParse_t* pp)
{
	// 写入目标文件
	zyfp_handle* ph = zyfp_init(pp->pf, pp->key, ZYFP_O_READ);
	if (!ph) {
		cprintf("读取归档文件出现错误\n", Co_RED);
		return;
	}
	if (!zyfp_passmatch(ph)) {
		cprintf("密钥不匹配，无法获取文件内容\n", Co_YELLOW);
		goto do_err;
	}
	ssize_t fs = zyfp_open(ph, pp->src);
	if (fs < 0) {
		printf("打开文件%s失败: %s\n", pp->src, zyfp_errmsg((int)fs));
		goto do_err;
	} else if (fs == 0) {
		printf("文件%s是空文件\n", pp->src);
		goto do_err;
	}
	char* fdata = NULL;
	fs = zyfp_readall(ph, &fdata);
	if (fs < 0) {
		printf("读取文件%s失败: %s\n", pp->src, zyfp_errmsg((int)fs));
		goto do_err;
	} else if (fs > 0){
		FILE* fp = fopen(pp->dir, "wb");
		if (!fp) {
			cprintf("无法写入目标文件", Co_RED);
			return;
		}
		fwrite(fdata, fs, 1, fp);
		free(fdata);
		cprintf("解档成功\n", Co_BLUE);
		fclose(fp);
	}
do_err:
	zyfp_end(ph);
}

static void do_unpack(const PackParse_t* pp)
{
	// 判定解档目录属性
	struct stat ds;
	if (stat(pp->dir, &ds) < 0) {
		if (ENOENT == errno) {
			// 尝试创建文件夹
			if (mkdir(pp->dir, 0755) != 0) {
				printf("创建解档目录出现错误:%s\n", strerror(errno));
				return;
			}
		} else {
			printf("测试解档目录出现错误:%s\n", strerror(errno));
			return;
		}
	} else {
		// 判断文件是否是文件夹，是否可写
		if (!S_ISDIR(ds.st_mode)) {
			cprintf("解档路径已存在，非文件夹\n", Co_RED);
			return;
		}
		if (!(ds.st_mode & (S_IWUSR | S_IXUSR))) {
			cprintf("解档路径已存在，但是没有写入和执行权限\n", Co_YELLOW);
			return;
		}
	}
	// 读取索引文件
	zyfpi_code(pp);
	FILE* fp = fopen(zyfpi, "rb");
	if (!fp) {
		cprintf("读取索引文件出现异常\n", Co_YELLOW);
		return;
	}
	uint32_t passl = 0;
	if (fread(&passl, sizeof(uint32_t), 1, fp) < 1) {
		cprintf("读取索引文件异常\n", Co_YELLOW);
		goto do_err;
	}
	char* key = NULL;
	if (passl) {
		key = (char*)malloc(passl + 1);
		if (fread(key, passl, 1, fp) < 1) {
			cprintf("读取索引文件中的密钥出现异常\n", Co_YELLOW);
			goto do_err;
		}
		key[passl] = 0;
	}
	// 载入数据包
	zyfp_handle* ph = zyfp_init(pp->pf, key, ZYFP_O_READ);
	if (key)
		free(key);
	if (!ph) {
		cprintf("打开归档文件失败\n", Co_RED);
		goto do_err;
	}
	if (!zyfp_passmatch(ph)) {
		cprintf("密钥不匹配，无法提取\n", Co_YELLOW);
		zyfp_end(ph);
		goto do_err;
	}
	if (chdir(pp->dir) < 0) {
		cprintf("切换工作目录出现异常\n", Co_BLUE);
		goto do_err;
	}
	char cd;
	char namebuf[10240];
	FILE* wfp;
	ssize_t rl;
	uint32_t needunpack = zyfp_packfilenum(ph);
	cprintf("开始尝试解档，请稍候\n", Co_GREEN);
	while (needunpack > 0) {
		fread(&cd, sizeof(cd), 1, fp);
		fread(&passl, sizeof(uint32_t), 1, fp);
		fread(namebuf, passl, 1, fp);
		namebuf[passl] = 0;
		if ('1' == cd) {
			// 创建文件夹
			if (mkdir(namebuf, 0755) < 0) {
				if (EEXIST != errno) {
					cprintf("解档创建文件夹时出现错误\n", Co_BLUE);
					break;
				}
			}
			continue;
		}
		if (zyfp_open_ex(ph, namebuf, passl) < 0) {
			cprintf("读取归档文件异常\n", Co_RED);
			break;
		}
		wfp = fopen(namebuf, "wb");
		rl = zyfp_read(ph, sizeof(namebuf), namebuf);
		while (rl > 0) {
			fwrite(namebuf, rl, 1, wfp);
			rl = zyfp_read(ph, sizeof(namebuf), namebuf);
		}
		fclose(wfp);
		--needunpack;
		cprintf(".", Co_BLUE);
	}
	if (needunpack == 0)
		cprintf("\n解档完成\n", Co_GREEN);
	zyfp_end(ph);
do_err:
	fclose(fp);
}

/*********************| main |*********************/
int main(int argc, const char* argv[])
{
	PackParse_t pp;
	Action_t action = parse_args(argc, argv, &pp);
	switch (action) {
	case AC_HELP:
		show_help();
		break;
	case AC_SHOWINFO:
		show_pack_info(&pp);
		break;
	case AC_PACK:
		do_pack(&pp);
		break;
	case AC_DUMP:
		do_dump(&pp);
		break;
	case AC_UNPACK:
		do_unpack(&pp);
		break;
	}
	return 0;
}
